/*
 * 代号：凤凰
 * http://www.jphenix.org
 * 2019年1月16日
 * V4.0
 */
package com.jphenix.servlet.service;

import com.jphenix.driver.json.Json;
import com.jphenix.servlet.parent.ActionBeanParent;
import com.jphenix.servlet.parent.ServiceBeanParent;
import com.jphenix.share.lang.SInteger;
import com.jphenix.share.lang.StringSorter;
import com.jphenix.share.tools.Base64;
import com.jphenix.share.tools.MD5;
import com.jphenix.share.util.BaseUtil;
import com.jphenix.standard.docs.BeanInfo;
import com.jphenix.standard.docs.ClassInfo;
import com.jphenix.standard.docs.Running;
import com.jphenix.standard.servlet.api.IRequest;
import com.jphenix.standard.servlet.api.IResponse;
import com.jphenix.standard.viewhandler.INodeHandler;
import com.jphenix.standard.viewhandler.IViewHandler;

import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 签名验签工具
 * com.jphenix.servlet.service.SignService
 * 
 * 
 * 配置文件段
 * 
 * <sign>
 *   <private_key>本地私钥字符串</private_key>
 *   <client name="客户端客户号（唯一）" type="0" ips="限制访问IP地址" >双方约定的盐值</client>
 *   <client name="客户端客户号（唯一）" type="1" ips="限制访问IP地址">客户端公钥字符串</client>
 *   <client name="客户端客户号（唯一）" type="2" ips="限制访问IP地址" pwd="验证秘钥的MD5值">双方约定的盐值</client>
 * </sign>
 * 
 * client 段属性说明：
 * name          客户端客户号，不能有重复值
 * type          0:md5盐值签名、验签  1:sha1签名、验签  2:md5(盐值+md5(密码))验证
 * from_url      为1时，验证信息全部从URL的参数中获取
 * header_key    默认值为sign_key。如果要求客户端提交签名串或密码，不叫sign_key，可以使用该属性，另起一个名字
 * ips           限制访问IP地址，支持通配符. 比如：ips="*" ips="192.168.*.*"
 *
 * 调用URL： 服务端url?client_key=客户端号   或  服务端url?_c=客户端号
 *           
 *           注意：不能以POST方式提交客户端号。因为服务端要考虑提交的信息是否为XML格式还是JSON格式，还是文本格式。
 *                 服务端程序需要在解析数据对象之前，就知道是哪个客户端配置信息，也好读取是否允许访问的地址，拦截
 *                 非法客户端
 *
 * 待签名数据拼装规则：
 *
 *  先将需要提交的参数的主键，按照有小到大排序（方法：Arrays.sort(arrs)）
 * 然后遍历排序后的主键获取对应的值，拼装成以下格式：
 * key1=value1&key2=value2&key3=value3&key4=value4
 *
 * 注意：参与排序的参数中，不包含提交签名串值的参数（比如默认的：sign_key）
 *
 * 签名串生成以及提交规则：
 * type为0 时， 客户客户端将准备提交的参数按照上面的格式组成待签名数据字符串，按照以下方式生成MD5编码。
 *              签名串 = MD5(待签名数据字符串+双方约定的盐值);
 *              然后将签名串作为sign_key的参数值一同提交到服务端。
 *
 * type为1时，  客户端将准备提交的数据按照上面格式组成待签名数据字符串，然后用客户端的私钥进行签名，生成签名串。
 *              然后将签名串作为sign_key的参数值一同提交到服务端。
 *
 * type为2时，  客户端只需要提交服务端分配的密码（提交密码的参数主键为：sign_key）
 *              密码加密方式为： MD5(盐值+MD5("客户端提交的密码")).equals("配置文件中，client节点pwd属性值")
 *              
 * 2019-01-24 从脚本转换为传统Java类
 * 2022-09-04 隔离了ServletApi，兼容新老Tomcat
 *
 * @author MBG
 * 2017年7月1日
 */
@ClassInfo({"2022-09-04 21:58","签名验签工具"})
@BeanInfo({"signservice"})
@Running({"80","init"})
public class SignService extends ServiceBeanParent {

	private PrivateKey privateKey = null; //常驻内存的私钥类
	
	 //客户端秘钥对照容器
	// key 客户机号
	// value:   0 签名类型  0：md5签名   1：sha1签名
	//             1 http报文头中 签名信息参数名
	//             2 客户端公钥字符串或md5盐值
	//             3 登录密码的MD5值
	private Map<String,Map<String,String>> clientInfoMap = null;
	
	//公钥主键对照容器 key：客户机号   value：公钥对象
	private Map<String,PublicKey> pubKeyMap = null;
	
	public String localPrivateKey = ""; //本地私钥

	/**
	 * 执行初始化
	 * @throws Exception 初始化异常
	 * 2017年7月2日
	 * @author MBG
	 */
	public void init() throws Exception {
		reloadConfig(); //重新加载配置文件
	}
	
	/**
	 * 重新加载配置文件
	 * 2017年7月2日
	 * @author MBG
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public void reloadConfig() {
		//获取签名信息配置参数段
		IViewHandler confVh      = px("sign");
		//私钥字符串
		localPrivateKey          = confVh.fnn("private_key").nt();
		pubKeyMap                = new HashMap<String,PublicKey>();
		clientInfoMap            = new HashMap<String,Map<String,String>>();
		//获取客户端配置信息
		List<IViewHandler> cList = confVh.cnn("client");
		Map eleMap; //参数容器
		List<String[]> allowIpList; //允许访问的IP地址序列（为空则允许所有地址访问）
		
		String   name;        //方案名
		String[] allowIps;    //设置允许访问的IP地址数组
		String[] ipInfos;     //拆分的ip信息数组
		String[] allowInfos;  //整理后允许访问的IP地址段数组
		for(IViewHandler evh : cList) {
			eleMap = new HashMap();
			name = evh.a("name");
			if(name.length()<1) {
				continue;
			}
			eleMap.put("name",name);
			eleMap.put("type",SInteger.stringValueOf(evh.a("type"))); //签名类型
			eleMap.put("key",evh.nt()); //MD5盐值，或者为客户机公钥
			eleMap.put("header_key",def(evh.a("header_key"),"sign_key")); //在http请求头中，签名串参数主键
			eleMap.put("pwd",evh.a("pwd")); //获取token的密码
			
			allowIpList = new ArrayList<String[]>();
			eleMap.put("allow_ips",allowIpList);
			
			allowIps = BaseUtil.split(evh.a("ips"),",");
			for(String ip:allowIps) {
				//处理允许
				ipInfos = BaseUtil.split(ip,".");
	        	if(ipInfos==null || ipInfos.length<1) {
	        		continue;
	        	}
	        	
				allowInfos = new String[4];
	        	if(ipInfos.length<2) {
	        		allowInfos[0] = str(ipInfos[0]);
	        		allowInfos[1]  = "*";
	        		allowInfos[2] = "*";
	        		allowInfos[3] = "*";
	        	}else if(ipInfos.length<3) {
	        		allowInfos[0] = str(ipInfos[0]);
	        		allowInfos[1]  = str(ipInfos[1]);
	        		allowInfos[2] = "*";
	        		allowInfos[3] = "*";
	        	}else if(ipInfos.length<4) {
	        		allowInfos[0] = str(ipInfos[0]);
	        		allowInfos[1]  = str(ipInfos[1]);
	        		allowInfos[2] = str(ipInfos[2]);
	        		allowInfos[3] = "*";
	        	}else {
	        		allowInfos[0] = str(ipInfos[0]);
	        		allowInfos[1]  = str(ipInfos[1]);
	        		allowInfos[2] = str(ipInfos[2]);
	        		allowInfos[3] = str(ipInfos[3]);
	        	}
	        	allowIpList.add(allowInfos);
			}
			clientInfoMap.put(name,eleMap);
		}
	}
	
	/**
	 * 执行验签
	 * @param clientKey 客户机号
	 * @param content   
	 * @return
	 * 2017年7月2日
	 * @author MBG
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public String verfySign(IRequest req,ActionBeanParent ab)  {
        //字符串方式提交参数
        String clientKey = req.getUrlParameter("client_key");
		if(clientKey.length()<1) {
          clientKey = req.getUrlParameter("_c");
          if(clientKey.length()<1){
            return "E001 没有获取到客户端标识 client_key";
          }
		}
        //方案配置信息
        Map info = clientInfoMap.get(clientKey);
		if(info==null) {
		  return "E002 没有获取到客户端注册的信息["+clientKey+"]";
		}
		
		if(!allow(req.cip(),(List<String[]>)info.get("allow_ips"))) {
			return "E006 非法访问的IP地址:["+req.cip()+"]";
		}
		
        //密文头主键值(签名串参数主键)
        String signKey   = str(info.get("header_key"));
        //密文值 或 提交数据签名串
        String signValue = str(req.getHeader(signKey));
        
        //是否从URL中获取参数值
        boolean fromUrl = boo(info.get("from_url"));
        
		if(signValue.length()<1) {
	        if(fromUrl) {
	        	//从URL中获取参数值
	        	signValue = req.getUrlParameter(signKey);
	        }else if(ab.getPostType()==0){
	          //字符串方式提交参数
	          signValue = str(ab.getActionContext().getParameter(signKey));
	        }else if(ab.getPostType()==1){
	          //XML方式提交参数
	          //获取提交的XML对象
	          INodeHandler nh = (INodeHandler)ab.getPostObject();
	          if(nh==null){
	            return "E010 没有获取到提交的XML信息";
	          }
	          signValue = nh.fnn(signKey).nt();
	        }else if(ab.getPostType()==2){
	          //Json方式提交参数
	          Json json = (Json)ab.getPostObject();
	          if(json==null){
	            return "E011 没有获取到提交的Json信息";
	          }
	          signValue    = json.value(signKey);
	        }else {
	          return "E012 未知提交类型值["+ab.getPostType()+"] 仅支持字符串，XML，Json格式数据提交";
	        }
		}
		if(signValue.length()<1) {
			return "E003 没有获取到提交数据签名串，主键名:["+signKey+"]";
		}
        String salt = str(info.get("key")); //盐值
		//签名、验签类型
		String signType = str(info.get("type"));
        debug("收到客户端：["+clientKey+"] 签名串：["+signValue+"] 签名类型：["+signType+"]");
		if("2".equals(signType)) {
			//用户提交密码方式验证  md5(盐值+md5(密码))  比对  配置文件中的密码MD5
			if(MD5.getMD5Value(salt+MD5.getMD5Value(signValue)).equals(info.get("pwd"))) {
				return null;
			}
			debug("签名串不匹配 fromUrl:["+fromUrl+"] type:["+signType+"] key:["+info.get("key")+"] header_value:["+signValue+"] pwd:["+info.get("pwd")+"]");
			debug("提交数据类型：["+ab.getPostType()+"]");
			debug("提交值：");
			debug(ab.getPostObject());
			return "E009 验签失败，签名串不匹配";
		}
		
		//带签名数据
		StringBuffer verfySbf = new StringBuffer();
      
		if(fromUrl) {
			//从请求URL中获取参数对照容器
			getData(req.getUrlParameterMap(),verfySbf,signKey);
		}else if(ab.getPostType()==0){
          //字符串方式提交的数据
          getData((Map<String,String>)ab.getPostObject(),verfySbf,signKey); //拼装准备签名字符串
        }else if(ab.getPostType()==1){
          //XML方式提交的数据
          getData(((INodeHandler)ab.getPostObject()).cnm(),verfySbf,signKey); //拼装准备签名字符串
        }else if(ab.getPostType()==2){
          //JSon方式提交的数据
          getData(((Json)ab.getPostObject()).valueMap(),verfySbf,signKey); //拼装准备签名字符串
        }else{
          //不可能走到这里，上面已经拦截掉了
        }
		if("0".equals(signType)) {
			//MD5方式验签
			return md5VerfySign(salt,verfySbf,signValue);
		}else if("1".equals(signType)) {
			//SHA1方式验签
			return sha1VerfySign(clientKey,verfySbf.toString(),signValue);
		}else {
			return "E007 未知验签类型：["+signType+"]";
		}
	}
	
	/**
	 * 判断请求
	 * @param req            客户端IP地址
	 * @param allowIpList  允许访问的Ip地址信息序列
	 * @return                   是否允许访问
	 * 2017年5月2日
	 * @author MBG
	 */
	public boolean allow(String ip,List<String[]> allowIpList) {
		if(allowIpList==null || ip==null || ip.length()<1) {
			return false;
		}
		if("0:0:0:0:0:0:0:1".equals(ip) || "127.0.0.1".equals(ip) || "localhost".equalsIgnoreCase(ip)) {
			return true;
		}
		//分割为ip段信息数组
		String[] ips = BaseUtil.split(ip,".");
		if(ips==null || ips.length<4) {
			//地址不合法
			return false;
		}
		for(String[] infos:allowIpList) {
			//第一段
			if("*".equals(infos[0])) {
				//麻痹的，配置信息中，第一步就是通配符，还做什么过滤
				return true;
			}
			if(!infos[0].equals(ips[0])) {
				continue;
			}
			//第二段
			if("*".equals(infos[1])) {
				return true;
			}
			if(!infos[1].equals(ips[1])) {
				continue;
			}
			//第三段
			if("*".equals(infos[2])) {
				return true;
			}
			if(!infos[2].equals(ips[2])) {
				continue;
			}
			//第四段（最后一段）
			if("*".equals(infos[3]) || infos[3].equals(ips[3])) {
				return true;
			}
		}
		return false;
	}
	
	
	/**
	 * MD5方式验签
	 * @param saltFigure  盐值
	 * @param verfyData  待签名数据
	 * @param sign          客户机签名串
	 * @return                  null:验证成功    不为空:验签失败原因
	 * 2017年7月3日
	 * @author MBG
	 */
	protected String md5VerfySign(String saltFigure,StringBuffer verfyData,String sign) {
		verfyData.append(saltFigure); //将盐值放入待验签数据末尾
		if(MD5.getMD5Value(verfyData.toString()).equals(sign.toUpperCase())) {
			return null;
		}
		return "E009 验签失败，签名串不匹配";
	}
	
	/**
	 * SHA1方式验签
	 * @param clientKey   客户机号
	 * @param pubKey     客户机公钥
	 * @param verfySbf    待验签字符串
	 * @param sign          客户机签名串
	 * @return                  null:验证成功    不为空:验签失败原因
	 * 2017年7月3日
	 * @author MBG
	 */
	protected String sha1VerfySign(String clientKey,String verfyData,String sign) {
		try {
            //有时候客户端服务器提交过来的验签值，类似证书的方式，其中带换行符
            //在这里将换行符去掉
            sign = BaseUtil.swapString(sign,"\r","","\n","");
			//获取指定客户机公钥
			PublicKey publicKey = getPublicKeyFromX509(clientKey);
			Signature signature = Signature.getInstance("SHA1WithRSA");
			signature.initVerify(publicKey);
			signature.update(verfyData.getBytes(StandardCharsets.UTF_8));

			if(signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8)))) {
				return null;
			}
			return "E009 验签失败，签名串不匹配";
		}catch(Exception e) {
			e.printStackTrace();
			return "E008 验签发生异常：" + e.toString();
		}
	}
	
	
	/**
	 * 获取公钥类
	 */
	private PublicKey getPublicKeyFromX509(String clientKey) throws Exception {
		//先从容器中获取已经构造好的公钥对象
		PublicKey reKey = pubKeyMap.get(clientKey);
		if(reKey==null){
          //获取目标服务器配置信息
          Map<String,String> objInfo = clientInfoMap.get(clientKey);
          if(objInfo==null){
            return null;
          }
          String keyStr = str(objInfo.get("key")); //客户端公钥
          KeyFactory keyFactory = KeyFactory.getInstance("RSA");
          byte[] encodeByte = Base64.decode(keyStr.getBytes());
          reKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodeByte));
          pubKeyMap.put(clientKey,reKey);
		}
		return reKey;
	}
	
	/**
	 * 获取准备签名的字符串
	 * @param paraMap    请求参数容器
	 * @param verfySbf   带签名的字符串
     * @param excludeKey 需要排除的参数主键（签名串主键）
	 * @return           准备签名的字符串
	 * 2017年7月2日
	 * @author MBG
	 */
	@SuppressWarnings("rawtypes")
	protected String getData(Map paraMap,StringBuffer verfySbf,String excludeKey) {
		//获取请求参数（有小到大排序）
		List<String> keyList = StringSorter.asc(BaseUtil.getMapKeyList(paraMap));
		boolean noFirst = false; //是否不是首次循环
		for(String keyEle:keyList) {
            if(excludeKey!=null && excludeKey.length()>0 && excludeKey.equals(keyEle) || "client_key".equals(keyEle) || "_c".equals(keyEle)){
              continue;
            }
			if(noFirst) {
				verfySbf.append("&");
			}else {
				noFirst = true;
			}
			verfySbf.append(keyEle).append("=").append(str(paraMap.get(keyEle)));
		}
		return verfySbf.toString();
	}
	
	/**
	 * 执行签名
	 * @param clientKey  客户机号
	 * @param dataMap    准备提交的数据包
	 * 2017年7月2日
	 * @author MBG
	 */
	public void sign(String objKey,Map<String,String> dataMap){
      if(dataMap==null){
        return;
      }
      //获取目标服务器配置信息
      Map<String,String> objInfo = clientInfoMap.get(objKey);
      if(objInfo==null){
        return;
      }
      String signKey = objInfo.get("header_key"); //签名串的参数名
      //准备签名的字符串
      String dataStr = getData(dataMap,new StringBuffer(),signKey);
      String signValue = sign(objKey,dataStr); //签名串值
      dataMap.put(signKey,signValue);
    }
	
		
	/**
	 * 执行签名
	 * @param clientKey  客户机号
	 * @param content    带签名字符串
	 * @return                 签名串
	 * 2017年7月2日
	 * @author MBG
	 */
	@SuppressWarnings("rawtypes")
	public String sign(String clientKey,String content) {
		if(clientKey==null || clientKey.length()<1 || content==null || content.length()<1) {
			return "";
		}
		//获取客户签名信息
		Map info = clientInfoMap.get(clientKey);
		if(info==null) {
			return "";
		}
		//签名、验签类型
		String type = str(info.get("type"));
		if("0".equals(type)) {
			//执行MD5签名
			return md5Sign(content,str(info.get("key")));
		}else if("1".equals(type)) {
			//执行SHA1签名
			return sha1Sign(content);
		}
		return "";
	}
	
	/**
	 * 签名并输出数据
	 * @param req             页面请求
	 * @param resp            页面反馈
	 * @param outObj        输出的数据
	 * 2017年7月6日
	 * @author MBG
	 */
	public void signOut(IRequest req,IResponse resp,Object outObj) {
		if(outObj==null) {
			return;
		}
        //之前遇到个无法呈现的问题，偶尔输出内容全是问号
        //原来使用的是vh.getDealEncode();这回写死UTF-8
        //基本输出编码 weblogic只能用基本编码输出
        String standardOutEncoding = "UTF-8";
        try {
            //设置编码格式
            resp.setCharacterEncoding(standardOutEncoding);
        }catch(NoSuchMethodError e) {
            //使用WebLogic时会抛异常到这里，然后使用标准输出
            //非weblogic不能使用标准输出，否则反而出现乱码
            standardOutEncoding = "ISO-8859-1";
        }
        String data = null; //输出数据
		//获取客户端主键
        //字符串方式提交参数
        String clientKey = req.getUrlParameter("client_key");
		if(clientKey.length()<1) {
          clientKey = req.getUrlParameter("_c");
          if(clientKey.length()<1){
            data = "E001 没有获取到客户端标识 client_key";
          }
		}
		@SuppressWarnings("rawtypes")
        //获取配置信息
		Map info = clientInfoMap.get(clientKey);
        if(data==null) {
		  //获取客户端信息
		  if(info==null) {
		  	data = "E002 没有获取到客户端注册的信息["+clientKey+"]";
		  }
        }
		if(data==null) {
			if(outObj instanceof IViewHandler) {
				//HTML XML
				data = ((IViewHandler)outObj).getNodeBody();
			}else if(outObj instanceof Json) {
				//Json
				data = outObj.toString();
			}else {
				//Other
				data = outObj.toString();
			}
		    //获取签名串
		    String signKey = sign(clientKey,data);
		    //放入返回报文头
		    resp.setHeader((String)info.get("header_key"),signKey);
            /** 注意：返回内容做签名，这块不成熟，暂时不用，仅对提交内容做签名验签 **/
		}else {
			//将报错信息拼装成所需要的格式
			if(outObj instanceof Json) {
				data = "{\"status\":\"-90\",\"msg\":\""+data+"\"}";
			}else {
				data = "<?xml version=\"1.0\" encoding='UTF-8'?><root><status>-90</status><msg><![CDATA["+data+"]]></msg></root>";
			}
		}
        try {
        	resp.getWriter().write(data);
            //不能输出html代码到页面，太乱
        }catch(Exception e) {}
	}
	
	
	/**
	 * 执行MD5签名
	 * @param content    待签名内容
	 * @param saltFigure 盐值
	 * @return                 签名串(大写)
	 * 2017年7月2日
	 * @author MBG
	 */
	protected String md5Sign(String content,String saltFigure) {
		return MD5.getMD5Value(content+saltFigure);
	}
	
	/**
	 * 执行sha1签名
	 * @param content 待签名内容
	 * @return              签名串
	 * 2017年7月2日
	 * @author MBG
	 */
	protected String sha1Sign(String content) {
		if(content.length()<1){
			return "";
		}
		try {
			//获取私钥对象
			PrivateKey privatekey = getPrivateKeyFromPKCS8();
			//构造签名类
			Signature signature = Signature.getInstance("SHA1WithRSA");
			signature.initSign(privatekey);
			signature.update(content.getBytes(StandardCharsets.UTF_8));

			//获取签名后的信息字节数组
			byte[] signed = signature.sign();
			
			//返回签名串
			return new String(Base64.encode(signed));
		}catch(Exception e) {
			e.printStackTrace();
		}
		return "";
	}
	
	/**
	 * 获取指定客户机信息
	 * @param clientKey 客户机号
	 * @return 客户机信息容器
	 * 2017年7月3日
	 * @author MBG
	 */
	@SuppressWarnings("rawtypes")
	public Map getClientInfo(String clientKey) {
		return clientInfoMap.get(clientKey);
	}
	
	/**
	 * 获取私钥对象
	 * @return 私钥对象
	 * @throws Exception 异常
	 * 2017年7月2日
	 * @author MBG
	 */
	private PrivateKey getPrivateKeyFromPKCS8() throws Exception {
	  if(privateKey==null){
	    //解密私钥
	    byte[] encodedKey = Base64.decode(localPrivateKey.getBytes());
	    //构建密钥工厂类
	    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
	    privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
	  }
	  return privateKey;
	}
}
